package guiservice.server.logic;

import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.kryonet.rmi.ObjectSpace;
import com.esotericsoftware.kryonet.rmi.RemoteObject;
import guiservice.common.controls.*;
import guiservice.common.controls.handlers.KeyEventArgs;
import guiservice.common.controls.handlers.MouseEventArgs;
import guiservice.common.net.Client;
import guiservice.common.net.ProtocolRegistration;
import guiservice.common.net.Server;
import guiservice.server.controls.ObjectFactory;
import lombok.Value;
import lombok.extern.log4j.Log4j;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

@Log4j
public class ServerImpl extends com.esotericsoftware.kryonet.Server implements Server {

    public static final int PORT = 6666;
    private final ObjectSpace serverObjectSpace = new ObjectSpace();
    private final AtomicLong counter = new AtomicLong(0);
    private final ConcurrentHashMap<Connection, Client> mapConnectionToClient = new ConcurrentHashMap<>();
    private final ConcurrentLinkedQueue<ClientAndUidPair> uidQueue = new ConcurrentLinkedQueue<>();
    private final Thread uidSenderThread = new Thread() {

        @Override
        public void run() {
            while (true) {
                try {
                    while (!uidQueue.isEmpty()) {
                        ClientAndUidPair element = uidQueue.poll();
                        element.getConnection().sendTCP(element.getUid());
                        log.debug("try set id");
                        Thread.sleep(1000);
                }
                Thread.sleep(1000);
                } catch (InterruptedException e) {
                    return;
                }
            }
        }

    };

    private static ServerImpl instance;

    public static ServerImpl getInstance() {
        if (instance == null) {
            instance = new ServerImpl();
        }
        return instance;
    }

    @Value
    private class ClientAndUidPair {
        long uid;
        Connection connection;
    }

    public static void main(String[] args) {
        ServerImpl serverImpl = new ServerImpl();
        serverImpl.start();
    }

    @Override
    public void start() {
        try {
            log.info("Program started...");

            bind(PORT);
            initialize();
            super.start();
            log.info("Server started...");
        } catch (Exception e) {
            log.error("Error occurred while starting server", e);
            log.info("Program finished");
        }
    }

    private void initialize() {
        uidSenderThread.start();
        ProtocolRegistration.registerDataObjects(this);
        serverObjectSpace.register(OBJECT_ID, this);
        serverObjectSpace.setExecutor(Executors.newCachedThreadPool());
        addListener(new Listener.ThreadedListener(new Listener() {

            @Override
            public void connected(Connection connection) {
                super.connected(connection);
                log.info(String.format("Connected from address %s", connection.getEndPoint()));
                serverObjectSpace.addConnection(connection);

                Client clientImpl = ObjectSpace.getRemoteObject(connection, Client.OBJECT_ID, Client.class);
                RemoteObject remoteObject = (RemoteObject) clientImpl;
                remoteObject.setNonBlocking(true);
                remoteObject.setTransmitReturnValue(false);
                remoteObject.setTransmitExceptions(false);

                mapConnectionToClient.put(connection, clientImpl);
                long uid = counter.incrementAndGet();
                log.info(String.format("Generated uid %d", uid));
                ControlsManager.getInstance().registerClient(clientImpl, uid);
                uidQueue.add(new ClientAndUidPair(uid, connection));
            }

            @Override
            public void disconnected(Connection connection) {
                super.disconnected(connection);
                log.info(String.format("Disconnected from address %s", connection.getEndPoint()));
                serverObjectSpace.removeConnection(connection);
            }
        }));
    }

    @Override
    public long registerControl(long owner, String name, long uidOfClient) {
        try {
            log.debug(String.format("registerControl owner=%d, name=%s, uidOfClient=%d", owner, name, uidOfClient));
            Class<?> cl = Class.forName(String.format("guiservice.server.controls.%s", name));
            Control control = (Control) ObjectFactory.populateObject(owner, cl);
            long id = control.getId();
            serverObjectSpace.register((int)id, control);
            log.debug(String.format("Control with type %s registered with id %d", name, id));
            ControlsManager.getInstance().registerControl(control, id, uidOfClient);
            return id;
        } catch (Exception e) {
            log.error("Class not found", e);
            return -1L;
        }
    }

    @Override
    public void invalidate() {
        ControlsManager.getInstance().updateUI();
    }

    @Override
    public void unregisterControls(java.lang.Long[] idsForRemove) {
        try {
            log.debug("unregisterControls");
            for (java.lang.Long id : idsForRemove) {
                ControlsManager.getInstance().unregisterControl(id);
                serverObjectSpace.remove(id);
            }
            invalidate();
        } catch (Exception e) {
            log.error("", e);
        }
    }

    @Override
    public java.lang.Long[] getClients() {
        return ControlsManager.getInstance().getClients();
    }

    @Override
    public Long[] getControls(long uid, String type) {
        return ControlsManager.getInstance().getControls(uid, String.format("guiservice.server.controls.%s", type));
    }

    @Override
    public void eventHandler(long objectId, long uid, MouseEventArgs args) {
        log.debug(String.format("eventHandler objectId=%d, uid=%d, args=%s", objectId, uid, args));
        ControlsManager.getInstance().getClientById(uid).eventHandler(objectId, args);
    }

    @Override
    public void eventHandler(long objectId, long uid, KeyEventArgs args) {
        ControlsManager.getInstance().getClientById(uid).eventHandler(objectId, args);
    }

}
